import { SinglyLinkedListNode } from './singlyLinkedListNode'
import {
  type IterableBaseOptions,
  IterableBase,
} from './iterable-base'

export type SinglyLinkedListOptions<R, V> = IterableBaseOptions<R, V>

export class SinglyLinkedList<V = any, R = any> extends IterableBase<R, V> {
  /** first node */
  #headNode?: SinglyLinkedListNode<V>

  /** last node */
  #tailNode?: SinglyLinkedListNode<V>

  /** node size */
  #sizeNode = 0

  /**
   * @example
   * new SinglyLinkedList([1, 2, 3], { toValueFn: (value) => value * 2 }) // ==> [2, 4, 6]
   */
  constructor(values: Iterable<V> | Iterable<R> = [], options?: SinglyLinkedListOptions<R, V>) {
    super(options)
    if (values) {
      for (const el of values) {
        if (this.getToValueFn) {
          this.push(this.getToValueFn(el as R))
        } else {
          this.push(el as V)
        }
      }
    }
  }

  /**
   * @description The getter `head` function returns the first node of a singly linked list.
   * @returns {SinglyLinkedListNode | undefined} a SinglyLinkedListNode object or undefined
   */
  get head(): SinglyLinkedListNode<V> | undefined {
    return this.#headNode
  }

  /**
   * @description interval method
   * @description The setter `#head` function set the first node of a singly linked list.
   * @returns {void}
   */
  set #head(node: SinglyLinkedListNode<V> | undefined) {
    this.#headNode = node
  }

  /**
   * @description The getter `tail` function returns the last node of a singly linked list.
   * @returns {SinglyLinkedListNode | undefined} a SinglyLinkedListNode object or undefined
   */
  get tail(): SinglyLinkedListNode<V> | undefined {
    return this.#tailNode
  }

  /**
   * @description interval method
   * @description The setter `#tail` function set the last node of a singly linked list.
   * @returns {void}
   */
  set #tail(node: SinglyLinkedListNode<V> | undefined) {
    this.#tailNode = node
  }

  /**
   * The getter `size` function returns the size of an object.
   * @returns {number} The size of the object, which is a number
   */
  get size(): number {
    return this.#sizeNode
  }

  /**
   * @description interval method
   * @description The setter `size` function set the size of an object.
   * @returns {void}
   */
  private set size(value: number) {
    this.#sizeNode = value
  }

  /**
   * @returns {V | undefined} The value of the first node in the linked list, or undefined if the linked list is empty.
   */
  get first(): V | undefined {
    return this.head?.value
  }

  /**
   * @returns {{V | undefined}} The value of the last node in the linked list, or undefined if the linked list is empty.
   */
  get last(): V | undefined {
    return this.tail?.value
  }

  /**
   * @description The `push` function adds a new node to the end of a singly linked list.
   * @param {V} value - value of SinglyLinkedListNode
   * @returns {boolean} The `push` method is returning a boolean value, `true`.
   */
  public push(value: V): boolean {
    const newNode = new SinglyLinkedListNode(value)
    if (!this.head) {
      this.#head = newNode
      this.#tail = newNode
    } else {
      this.tail!.next = newNode
      this.#tail = newNode
    }
    this.size++
    return true
  }

  /**
   * @description The `pop` function removes last node in a linked list.
   * @returns {V | undefined} The value of the removed node.
   */
  public pop(): V | undefined {
    if (!this.head) return undefined
    if (this.head === this.tail) {
      const { value } = this.head
      this.#head = undefined
      this.#tail = undefined
      this.size--
      return value
    }

    let current = this.head
    while (current.next !== this.tail) {
      current = current.next!
    }
    const { value } = this.tail!
    current.next = undefined
    this.#tail = current
    this.size--
    return value
  }

  /**
   * @description The `unshift` function adds a new node to the beginning of a singly linked list.
   * @param value {V} - value of SinglyLinkedListNode.
   * @returns {boolean} The `unshift` method is returning a boolean value, `true`.
   */
  unshift(value: V): boolean {
    const newNode = new SinglyLinkedListNode(value)
    if (!this.head) {
      this.#head = newNode
      this.#tail = newNode
    } else {
      newNode.next = this.head
      this.#head = newNode
    }
    this.size++
    return true
  }

  /**
   * @description The `shift()` function removes first node.
   * @returns {V | undefined} The value of the removed node.
   */
  shift(): V | undefined {
    if (!this.head) return undefined
    if (this.head === this.tail) {
      const { value } = this.head
      this.#head = undefined
      this.#tail = undefined
      this.size--
      return value
    }
    const { value } = this.head
    this.#head = this.head.next
    this.size--
    return value
  }

  /**
   * @description The function `at` returns the value at a specified index in a linked list, or undefined if the index is out of range.
   * @param {number} index - The index parameter is a number that represents the position of the element we want to retrieve from the list. from `0` to `size - 1`.
   * @returns {V | undefined} the value at the specified index in the linked list, or `undefined` if the index is out of bounds.
   */
  public at(index: number): V | undefined {
    if (index < 0 || index >= this.size) return undefined
    let current = this.head!
    for (let i = 0; i < index; i++) {
      current = current.next!
    }
    return current.value
  }

  /**
   * @description The function `getNodeAt` returns the node at a given index in a singly linked list.
   * @param {number} index - The `index` parameter is a number that represents the position of the node we want to retrieve from the list. from `0` to `size - 1`.
   * @returns The `SinglyLinkedListNode<V>` object at the specified index exists, or `undefined` if the index is out of bounds.
   */
  public getNodeAt(index: number): SinglyLinkedListNode<V> | undefined {
    if (index < 0 || index >= this.size) return undefined
    let current = this.head
    for (let i = 0; i < index; i++) {
      current = current!.next
    }
    return current
  }

  /**
   * @description The `getNode` function finds a node in a singly linked list by its value
   * @param {V} value - the value that we want to search for in the linked list.
   * @returns {SinglyLinkedListNode<V> | undefined} a `SinglyLinkedListNode<V>` if a node with the specified value is found in the linked list. If no node with the specified value is found, the function returns `undefined`.
   */
  public getNode(value: V): SinglyLinkedListNode<V> | undefined {
    let current = this.head

    while (current) {
      if (current.value === value) {
        return current
      }
      current = current.next
    }

    return undefined
  }

  /**
   * @description The `deleteAt` function removes an node at a specified index from a linked list.
   * @param {number} index - The `index` parameter represents the position of the node that needs to be deleted.
   * @returns {boolean} if delete node success `true`, or `false` if the index is out of bounds.
   */
  public deleteAt(index: number): boolean {
    if (index < 0 || index >= this.size) return false
    if (index === 0) {
      this.shift()
      return true
    }
    if (index === this.size - 1) {
      this.pop()
      return true
    }
    const prevNode = this.getNodeAt(index - 1)
    const removedNode = prevNode!.next
    prevNode!.next = removedNode!.next
    this.size--
    return true
  }

  /**
   * @description The `delete` function removes a node with a specific value from a singly linked list.
   * @param {V | SinglyLinkedListNode<V>} valueOrNode - The `valueOrNode` parameter can accept either a value of type `V` or a `SinglyLinkedListNode<V>` node.
   * @returns {boolean} It returns `true` if the value or node is found and successfully deleted from the linked list and `false` if the value or node is not found in the linked list.
   */
  public delete(valueOrNode: V | SinglyLinkedListNode<V>): boolean {
    let value: V
    if (valueOrNode instanceof SinglyLinkedListNode) {
      value = valueOrNode.value
    } else {
      value = valueOrNode
    }
    let current = this.head
    let prev

    while (current) {
      if (current.value === value) {
        if (prev === undefined) { // first node match
          this.#head = current.next
          if (current === this.tail) { // only one node
            this.#tail = undefined
          }
        } else {
          prev.next = current.next
          if (current === this.tail) { // last node match
            this.#tail = prev
          }
        }
        this.size--
        return true
      }
      prev = current
      current = current.next
    }

    return false
  }

  /**
   * @description The `addAt` function inserts a value at a specified index in a singly linked list.
   * @param {number} index - position at which the new node with value should be inserted in the linked list.
   * @param {V} value - the value of node that you want to insert into the linked list at the specified index.
   * @returns {boolean} a boolean value. It returns `true` if the insertion is successful, and `false` if the index is out of bounds.
   */
  public addAt(index: number, value: V): boolean {
    if (index < 0 || index > this.size) return false
    if (index === 0) {
      this.unshift(value)
      return true
    }
    if (index === this.size) {
      this.push(value)
      return true
    }

    const newNode = new SinglyLinkedListNode(value)
    const prevNode = this.getNodeAt(index - 1)
    newNode.next = prevNode!.next
    prevNode!.next = newNode
    this.size++
    return true
  }

  /**
   * @description The `addBefore` function inserts a new value of node before an existing value of node in a singly linked list.
   * @param {V | SinglyLinkedListNode<V>} existingValueOrNode - The existing value or node that you want to insert the new value of node before. It can be either the value itself or a node containing the value in the linked list.
   * @param {v} newValue - the value of node that you want to insert into the linked list.
   * @returns {boolean} a boolean value. It returns `true` if the new value was successfully inserted before the existing value, and `false` otherwise.
   */
  public addBefore(existingValueOrNode: V | SinglyLinkedListNode<V>, newValue: V): boolean {
    if (!this.head) return false

    const existingValue = existingValueOrNode instanceof SinglyLinkedListNode
      ? existingValueOrNode.value
      : existingValueOrNode

    if (this.head.value === existingValue) {
      this.unshift(newValue)
      return true
    }

    let current = this.head
    while (current.next) {
      if (current.next.value === existingValue) {
        const newNode = new SinglyLinkedListNode(newValue)
        newNode.next = current.next
        current.next = newNode
        this.size++
        return true
      }
      current = current.next
    }

    return false
  }

  /**
   * @description The `addAfter` function inserts a new value of node after an existing node in a singly linked list.
   * @param {V | SinglyLinkedListNode<V>} existingValueOrNode - The existing value or node in the linked list after which the new value of node will be inserted. It can be either the value of the existing node or the existing node itself.
   * @param {V} newValue - The value of node that you want to insert into the linked list after the existing value or node.
   * @returns {boolean} It returns true if the new value was successfully inserted after the existing value or node, and false if the existing value or node was not found in the linked list.
   */
  public addAfter(existingValueOrNode: V | SinglyLinkedListNode<V>, newValue: V): boolean {
    const existingNode = existingValueOrNode instanceof SinglyLinkedListNode
      ? existingValueOrNode
      : this.getNode(existingValueOrNode)

    if (existingNode) {
      const newNode = new SinglyLinkedListNode(newValue)
      newNode.next = existingNode.next
      existingNode.next = newNode
      if (existingNode === this.tail) {
        this.#tail = newNode
      }
      this.size++
      return true
    }

    return false
  }

  /**
   * @description The `isEmpty` function checks if the length of a data structure is equal to zero
   * @returns {boolean}
   */
  public isEmpty(): boolean {
    return this.size === 0
  }

  /**
   * The `indexOf` function returns the index of the first occurrence of a given value in a linked list.
   * @param {V} value - the value that you want to find the index of in the linked list.
   * @returns {number} index of the first occurrence of the specified value in the linked list. If the value is not found, it returns -1.
   */
  public indexOf(value: V): number {
    let index = 0
    let current = this.head

    while (current) {
      if (current.value === value) {
        return index
      }
      index++
      current = current.next
    }

    return -1
  }

  /**
   * @description The `countOccurrences` function counts the number of occurrences of a given value in a linked list.
   * @param {V} value - The value parameter is the value that you want to count the occurrences of in the linked list.
   * @returns {number} The count of occurrences of the given value in the linked list.
   */
  public countOccurrences(value: V): number {
    let count = 0
    let current = this.head

    while (current) {
      if (current.value === value) {
        count++
      }
      current = current.next
    }

    return count
  }

  /**
   * @description The `clear` function resets the linked list by setting the head, tail, size.
   * @returns {void}
   */
  public clear(): void {
    this.#head = undefined
    this.#tail = undefined
    this.size = 0
  }

  /**
   * @description The `toArray` function converts a linked list into an array.
   * @returns {V[]} The `toArray()` method is returning an array of type `V[]`.
   */
  public toArray(): V[] {
    const array: V[] = []
    let current = this.head
    while (current) {
      array.push(current.value)
      current = current.next
    }
    return array
  }

  /**
   * @description The `reverse` function reverses the order of the nodes in a singly linked list.
   * @returns {void}
   */
  public reverse(): this {
    if (!this.head || this.head === this.tail) return this

    let prev: SinglyLinkedListNode<V> | undefined
    let current: SinglyLinkedListNode<V> | undefined = this.head
    let next: SinglyLinkedListNode<V> | undefined

    while (current) {
      next = current.next
      current.next = prev
      prev = current
      current = next
    }

    [this.#head, this.#tail] = [this.tail!, this.head!]
    return this
  }

  /**
   * @description The `clone` function returns a new instance of the `SinglyLinkedList` class with the same values as the original list.
   * @returns {SinglyLinkedList<V, R>} a new instance of the `SinglyLinkedList` class, which is a clone of the original list.
   */
  public clone(): SinglyLinkedList<V, R> {
    return new SinglyLinkedList<V, R>(this, { toValueFn: this.getToValueFn })
  }

  /**
   * @description The `fromArray` function creates a new SinglyLinkedList instance and populates it with the value from the given array.
   * @param {V[]} data - The `data` parameter is an array of value of type `V`.
   * @returns {SinglyLinkedList<V>}The `fromArray` function returns a `SinglyLinkedList` object.
   */
  public static fromArray<V>(data: V[]): SinglyLinkedList<V> {
    const singlyLinkedList = new SinglyLinkedList<V>()
    for (const item of data) {
      singlyLinkedList.push(item)
    }
    return singlyLinkedList
  }

  protected* getIterator(): IterableIterator<V> {
    let current = this.head

    while (current) {
      yield current.value
      current = current.next
    }
  }
}
